package spaska.data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Vector;

import spaska.data.Attribute.ValueType;

/**
 * 
 * @author <a href="mailto:vesko.m.georgiev@gmail.com">Vesko Georgiev<a>
 */
public class Dataset implements Iterable<Instance> {

	/**  */
	private String name;
	/**  */
	private List<Attribute> attributes;
	/**  */
	private List<Instance> instances;
	/** */
	private Map<Attribute, Set<Value>> attributeDomains;
	/** */
	private int classIndex;
	private int[] evalIdcs;

	public Dataset(String name, List<Attribute> attributes) {
		this(name, (Attribute[]) attributes.toArray(new Attribute[attributes.size()]));
	}

	public Dataset(String name, Attribute[] attributes) {
		this.name = name;
		this.attributes = new Vector<Attribute>();
		for (Attribute e : attributes) {
			this.attributes.add(e);
		}
		instances = new Vector<Instance>();
		attributeDomains = new HashMap<Attribute, Set<Value>>();
		classIndex = attributes.length - 1;
	}

	public Dataset() {
		instances = new Vector<Instance>();
		attributeDomains = new HashMap<Attribute, Set<Value>>();
		attributes = new LinkedList<Attribute>();
	}

	public Set<Value> getClassDomainValues() {
		return attributeDomains.get(getClassAttribute());
	}

	public Value[] getClassDomainArray() {
		return attributeDomains.get(getClassAttribute()).toArray(new Value[0]);
	}

	public String[] getClassDomainValuesArray() {
		Set<Value> classDomain = attributeDomains.get(getClassAttribute());
		if (classDomain != null) {
			String result[] = new String[classDomain.size()];
			int i = 0;
			for (Value v : classDomain) {
				result[i++] = v.toString();
			}
			return result;
		}
		return null;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setClassIndex(int classIndex) {
		this.classIndex = classIndex;
		updateInstances();
		Attribute a = getClassAttribute();
		if (a.getType() == ValueType.Numeric) {
			LinkedHashSet<Value> classDomain = new LinkedHashSet<Value>();
			for (Instance i : this) {
				classDomain.add(i.classValue());
			}
			setAttributeDomain(a, classDomain);
		}
	}

	public int getClassIndex() {
		return classIndex;
	}

	public List<Attribute> getAttributes() {
		return attributes;
	}

	public Attribute getAttribute(int idx) {
		return attributes.get(idx);
	}

	public Attribute getClassAttribute() {
		return attributes.get(classIndex);
	}

	public void addAttribute(Attribute... attributes) {
		for (Attribute attribute : attributes) {
			this.attributes.add(attribute);
		}
	}

	public Set<Value> getDomain(Attribute a) {
		return attributeDomains.get(a);
	}

	public Set<Value> getDomain(int attrIdx) {
		return getDomain(getAttribute(attrIdx));
	}

	public void setAttributeDomain(Attribute a, LinkedHashSet<Value> domain) {
		attributeDomains.put(a, domain);
	}

	public int getAttributesCount() {
		return attributes.size();
	}

	public ValueType[] getAttributeTypes() {
		ValueType[] attrTypes = new ValueType[getAttributesCount()];
		for (int i = 0; i < attrTypes.length; i++) {
			attrTypes[i] = getAttributes().get(i).getType();
		}
		return attrTypes;
	}

	public int size() {
		return instances.size();
	}

	public List<Instance> getElements() {
		return instances;
	}

	public void addElement(Instance e) {
		instances.add(e);
	}

	public void addElements(List<Instance> e) {
		for (Instance instance : e) {
			addElement(instance);
		}
	}

	/**
	 * @return all attribute indices except {@link #classIndex}
	 */
	public int[] getEvaluationIndices() {
		if (evalIdcs == null) {
			int[] inds = new int[attributes.size()];
			int idx = 0;
			for (int i = 0; i < attributes.size(); i++) {
				if (i != classIndex) {
					inds[idx++] = i;
				}
			}
			if (idx < inds.length) {
				inds = Arrays.copyOf(inds, inds.length - 1);
			}
			evalIdcs = inds;
		}
		return evalIdcs;
	}

	public void setEvaluationIndices(int... evalIdcs) {
		this.evalIdcs = evalIdcs;
	}

	@Override
	public String toString() {
		StringBuffer buff = new StringBuffer();
		buff.append(getName() + ": ");
		for (Attribute a : attributes) {
			buff.append(a + ", ");
		}
		buff.setCharAt(buff.length() - 2, '\n');
		buff.setLength(buff.length() - 1);

		for (Instance val : instances) {
			buff.append(val + "\n");
		}
		return buff.toString();
	}

	public static Dataset merge(Dataset[] datasets) {
		Dataset result = datasets[0].createCopyWithoutInstances();
		List<Instance> resultList = new ArrayList<Instance>();
		for (int i = 0; i < datasets.length; i++) {
			resultList.addAll(datasets[i].getElements());
		}
		result.instances = resultList;
		return result;
	}

	public Dataset[] getSubDataSets(int[] datasetCounts, int seed) {
		Random rand = new Random(seed);
		Dataset copy = this.createCopy();
		Dataset[] datasets = new Dataset[datasetCounts.length];
		for (int i = 0; i < datasets.length; i++) {
			datasets[i] = createCopyWithoutInstances();
		}
		Comparator<Instance> c = new Comparator<Instance>() {

			@Override
			public int compare(Instance o1, Instance o2) {
				return o1.classValue().compareTo(o2.classValue());
			}

		};
		Collections.sort(copy.instances, c);

		// randomize equal groups of classes
		List<Instance> instances = copy.instances;
		int instancesSize = instances.size();
		Value previousClassValue = instances.get(instancesSize - 1).classValue();
		int previousClassValueIndex = instancesSize - 1;
		while (instancesSize-- > 0) {
			Value currentValue = instances.get(instancesSize).classValue();
			if (!currentValue.equals(previousClassValue)) {
				List<Instance> instancesSubList = instances.subList(instancesSize, previousClassValueIndex);
				Collections.shuffle(instancesSubList, rand);
				previousClassValueIndex = instancesSize;
			}
		}

		int cnt = 0;
		List<Instance> allElements = copy.getElements();
		int cycles = allElements.size();
		while (cycles-- > 0) {
			int ind = cnt++ % datasetCounts.length;
			datasets[ind].addElement(allElements.get(cycles));
			datasetCounts[ind]--;
		}
		return datasets;
	}

	private void updateInstances() {
		for (Instance inst : instances) {
			inst.setClassIndex(classIndex);
		}
	}

	private Dataset createCopy() {
		Dataset result = new Dataset();
		result.name = name;
		result.attributes = Collections.unmodifiableList(this.attributes);

		result.instances.addAll(instances);
		result.attributeDomains = Collections.unmodifiableMap(this.attributeDomains);
		result.classIndex = classIndex;
		return result;
	}

	private Dataset createCopyWithoutInstances() {
		Dataset result = new Dataset();
		result.name = name;
		result.attributes = Collections.unmodifiableList(this.attributes);
		result.attributeDomains = Collections.unmodifiableMap(this.attributeDomains);
		result.classIndex = classIndex;
		return result;
	}

	@Override
	public Iterator<Instance> iterator() {
		return instances.iterator();
	}

}
